
#region Using Statements
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System.Diagnostics;
#endregion

namespace Tomahawk.Runtime.Rendering
{
    /// <summary>
    /// This is a special scene node type. 
    /// It does the regular SceneNode taks, but can also handle a camera
    /// </summary>
    public class Camera
    {

        public const float DEFAULT_FOVX = 90.0f;
        public const float DEFAULT_ZFAR = 1000.0f;
        public const float DEFAULT_ZNEAR = 1.0f;

        private float fovx;
        private float znear;
        private float zfar;

        private Vector3 eye;
        private Vector3 xAxis;
        private Vector3 yAxis;
        private Vector3 zAxis;

        private Matrix viewMatrix;
        private Matrix projMatrix;

        private float accumPitchDegrees;
        private float accumHeadingDegrees;

        #region "Public interface"

        public Matrix ViewMatrix
        {
            get { return viewMatrix; }
        }

        public Matrix ProjectionMatrix
        {
            get { return projMatrix; }
        }

        public Vector3 EyePosition
        {
            get { return this.eye; }
            set
            {
                this.eye = value;
                this.UpdateViewMatrix(false);
            }
        }

        /// <summary>
        /// Returns the camera's local X axis.
        /// </summary>
        public Vector3 XAxis
        {
            get { return xAxis; }
        }

        /// <summary>
        /// Returns the camera's local Y axis.
        /// </summary>
        public Vector3 YAxis
        {
            get { return yAxis; }
        }

        /// <summary>
        /// Returns the camera's local Z axis.
        /// </summary>
        public Vector3 ZAxis
        {
            get { return zAxis; }
        }

        /// <summary>
        /// Return the camera's view vector. The view vector is the direction
        /// the camera is looking. Since XNA uses a right handed coordinate
        /// system the view vector is the inverse of the camera's local z axis.
        /// </summary>
        public Vector3 ViewVector
        {
            get { return Vector3.Negate(zAxis); }
        }

        #endregion

        /// <summary>
        /// Constructor
        /// </summary>
        public Camera()
        {
            //default position and lookin'
            eye = new Vector3(0.0f, 0.0f, 0.0f);
            xAxis = Vector3.UnitX;
            yAxis = Vector3.UnitY;
            zAxis = Vector3.UnitZ;

            //default aspect ratio
            int w = Engine.Instance.GraphicDeviceManager.GraphicsDevice.Viewport.Width;
            int h = Engine.Instance.GraphicDeviceManager.GraphicsDevice.Viewport.Height;
            float aspect = (float)w / (float)h;

            // Setup initial default view and projection matrices.
            Perspective(DEFAULT_FOVX, aspect, DEFAULT_ZNEAR, DEFAULT_ZFAR);
            viewMatrix = Matrix.Identity;

        }

        /// <summary>
        /// Builds a perspective projection matrix based on a horizontal field
        /// of view. The aspect ratio is calculated by dividing the viewport's
        /// width by its height.
        /// </summary>
        /// <param name="fovx">Horizontal field of view in degrees.</param>
        /// <param name="aspect">Aspect ratio.</param>
        /// <param name="znear">Near plane distance.</param>
        /// <param name="zfar">Far plane distance.</param>
        public void Perspective(float fovx, float aspect, float znear, float zfar)
        {
            this.fovx = fovx;
            this.znear = znear;
            this.zfar = zfar;

            float e = 1.0f / (float)Math.Tan(MathHelper.ToRadians(fovx) / 2.0f);
            float aspectInv = 1.0f / aspect;
            float fovy = 2.0f * (float)Math.Atan(aspectInv / e);
            float xScale = 1.0f / (float)Math.Tan(0.5f * fovy);
            float yScale = xScale / aspectInv;

            projMatrix.M11 = xScale;
            projMatrix.M12 = 0.0f;
            projMatrix.M13 = 0.0f;
            projMatrix.M14 = 0.0f;

            projMatrix.M21 = 0.0f;
            projMatrix.M22 = yScale;
            projMatrix.M23 = 0.0f;
            projMatrix.M24 = 0.0f;

            projMatrix.M31 = 0.0f;
            projMatrix.M32 = 0.0f;
            projMatrix.M33 = (zfar + znear) / (znear - zfar);
            projMatrix.M34 = -1.0f;

            projMatrix.M41 = 0.0f;
            projMatrix.M42 = 0.0f;
            projMatrix.M43 = (2.0f * zfar * znear) / (znear - zfar);
            projMatrix.M44 = 0.0f;
        }

        /// <summary>
        /// Builds a view matrix. This method is analogous to the
        /// Matrix.CreateLookAt() method.
        /// </summary>
        /// <param name="eye">The eye position.</param>
        /// <param name="at">The target position.</param>
        /// <param name="up">The world's up direction.</param>
        public void LookAt(Vector3 eyePosition, Vector3 at, Vector3 up)
        {
            this.eye = eyePosition;
            //eyeLevelStanding = eye.Y;

            zAxis = eye - at;
            zAxis.Normalize();

            Vector3.Cross(ref up, ref zAxis, out xAxis);
            xAxis.Normalize();

            Vector3.Cross(ref zAxis, ref xAxis, out yAxis);
            yAxis.Normalize();
            xAxis.Normalize();

            viewMatrix.M11 = xAxis.X;
            viewMatrix.M21 = xAxis.Y;
            viewMatrix.M31 = xAxis.Z;
            Vector3.Dot(ref xAxis, ref eye, out viewMatrix.M41);
            viewMatrix.M41 = -viewMatrix.M41;

            viewMatrix.M12 = yAxis.X;
            viewMatrix.M22 = yAxis.Y;
            viewMatrix.M32 = yAxis.Z;
            Vector3.Dot(ref yAxis, ref eye, out viewMatrix.M42);
            viewMatrix.M42 = -viewMatrix.M42;

            viewMatrix.M13 = zAxis.X;
            viewMatrix.M23 = zAxis.Y;
            viewMatrix.M33 = zAxis.Z;
            Vector3.Dot(ref zAxis, ref eye, out viewMatrix.M43);
            viewMatrix.M43 = -viewMatrix.M43;

            viewMatrix.M14 = 0.0f;
            viewMatrix.M24 = 0.0f;
            viewMatrix.M34 = 0.0f;
            viewMatrix.M44 = 1.0f;

            accumPitchDegrees = MathHelper.ToDegrees((float)-Math.Asin(viewMatrix.M23));
            accumHeadingDegrees = MathHelper.ToDegrees((float)-Math.Atan2(viewMatrix.M13, viewMatrix.M33));

        }

        /// <summary>
        /// Reconstructs the view matrix. This is usually performed in response
        /// to rotations and when the camera's eye position is changed.
        /// </summary>
        /// <param name="orthogonalize">Whether to orthogonalized the axes.</param>
        private void UpdateViewMatrix(bool orthogonalize)
        {

            if (orthogonalize)
            {
                // Regenerate the camera's local axes to orthogonalize them.
                zAxis.Normalize();
                Vector3.Cross(ref zAxis, ref xAxis, out yAxis);
                yAxis.Normalize();
                Vector3.Cross(ref yAxis, ref zAxis, out xAxis);
                xAxis.Normalize();
            }

            viewMatrix.M11 = xAxis.X;
            viewMatrix.M21 = xAxis.Y;
            viewMatrix.M31 = xAxis.Z;
            Vector3.Dot(ref xAxis, ref eye, out viewMatrix.M41);
            viewMatrix.M41 = -viewMatrix.M41;

            viewMatrix.M12 = yAxis.X;
            viewMatrix.M22 = yAxis.Y;
            viewMatrix.M32 = yAxis.Z;
            Vector3.Dot(ref yAxis, ref eye, out viewMatrix.M42);
            viewMatrix.M42 = -viewMatrix.M42;

            viewMatrix.M13 = zAxis.X;
            viewMatrix.M23 = zAxis.Y;
            viewMatrix.M33 = zAxis.Z;
            Vector3.Dot(ref zAxis, ref eye, out viewMatrix.M43);
            viewMatrix.M43 = -viewMatrix.M43;

            viewMatrix.M14 = 0.0f;
            viewMatrix.M24 = 0.0f;
            viewMatrix.M34 = 0.0f;
            viewMatrix.M44 = 1.0f;
        }

        /// <summary>
        /// Rotates the camera but constrains heading rotations to be about the
        /// world Y axis. This is how rotations are performed for first person
        /// style cameras where the pitching is limited to looking straight up
        /// and straight down and heading rotations are always about the world
        /// Y axis. Roll is often used to implement leaning but it is not
        /// implemented here. Rotations are measured in degrees counter
        /// clockwise when looking along the rotation axis towards the origin.
        /// </summary>
        /// <param name="head">Heading in degrees.</param>
        /// <param name="pitch">Pitch in degrees.</param>
        private void Rotate(float head, float pitch)
        {
            accumPitchDegrees += pitch;

            if (accumPitchDegrees > 90.0f)
            {
                pitch = 90.0f - (accumPitchDegrees - pitch);
                accumPitchDegrees = 90.0f;
            }

            if (accumPitchDegrees < -90.0f)
            {
                pitch = -90.0f - (accumPitchDegrees - pitch);
                accumPitchDegrees = -90.0f;
            }

            accumHeadingDegrees += head;

            if (accumHeadingDegrees > 360.0f)
                accumHeadingDegrees -= 360.0f;

            if (accumHeadingDegrees < -360.0f)
                accumHeadingDegrees += 360.0f;

            Matrix rotMtx;

            // Rotate camera's existing x and z axes about the world y axis.
            if (head != 0.0f)
            {
                rotMtx = Matrix.CreateFromAxisAngle(Vector3.UnitY, MathHelper.ToRadians(head));

                xAxis = Vector3.Transform(xAxis, rotMtx);
                zAxis = Vector3.Transform(zAxis, rotMtx);
            }

            // Rotate camera's existing y and z axes about its existing x axis.
            if (pitch != 0.0f)
            {
                rotMtx = Matrix.CreateFromAxisAngle(xAxis, MathHelper.ToRadians(pitch));

                yAxis = Vector3.Transform(yAxis, rotMtx);
                zAxis = Vector3.Transform(zAxis, rotMtx);
            }

            UpdateViewMatrix(true);
        }

        #region "DEBUG: Controlling camera by GamePadState"

        public void DebugFPSUpdate(GamePadState gamePad)
        {

            this.Rotate(gamePad.ThumbSticks.Left.X * -1, gamePad.ThumbSticks.Left.Y);

            this.EyePosition +=
                Vector3.Multiply(this.ViewVector, gamePad.ThumbSticks.Right.Y * 4) +
                Vector3.Multiply(this.XAxis, gamePad.ThumbSticks.Right.X * 4);

            //move up / down
            if (gamePad.DPad.Up == ButtonState.Pressed)
                this.EyePosition += Vector3.Multiply(this.YAxis, 4);

            if (gamePad.DPad.Down == ButtonState.Pressed)
                this.EyePosition += Vector3.Multiply(this.YAxis, -4);

            this.UpdateViewMatrix(false);

            //look at 0,0,0
            if (gamePad.Buttons.RightShoulder == ButtonState.Pressed)
            {
                this.LookAt(this.EyePosition, new Vector3(0, 0, 0), Vector3.UnitY);
            }

        }

        #endregion
    }

}


